#! /usr/bin/python


import os
import json
import argparse
import xlwt
# import dicttoxml
# from xml.dom.minidom import parseString

NAME_FORMAT_ERROR = r"format error, naming rule: @{organization}/{component_name}."
COMPONENT_NAME_TOO_LONG = r"maximum length is 63."
NAME_NOT_LOWER = r"should be all lowercase letters."

VERSION_ERROR = r"value incorrect, please check."
ONLY_ONE_DESTPATH = r"'not list."

COMPONENT_NAME_ERROR = r"should same with 'name' field's component part."
SUBSYSTEM_MUST_LOWER = r"should consist of only lowercase letters."
SYSCAP_HEADER_ERROR = r"should begin with 'SystemCapability.'"
PASCAL_CASING_STYLE = r"every word that the first letter is capitalized."
SYSCAP_FORMAT_ERROR = r"format error. rule: SystemCapability.{Subsystem}.{ComponentCapbility}[.{SubCapbility}] "
FEATURE_SEPARATOR_ERROR = r"use '_' to separate component name and feature name"
FEATURE_PREFIX_ERROR = r"prefix should same with component name."

ERROR_EMPTY = r"can't be empty."
NO_FIELD = r"no such field."

def check_bundle_componnet_deps():
    return ""

def islower(s:str):
    '''
    @func: 判断 s 是否全小写字母。
    @note: 不能包含其他字符。
    '''
    if not s:
        return False
    for i in s:
        if not (97 <= ord(i) <= 122):
            return False
    return True

def str2value(s:str):
    '''
    @func: 将 s 字符串的数字部分和单位分离
    '''
    if s[0] == '~': # 去除 ram 前面的 '~' 字符
        s = s[1:]
    if s.isdigit():
        return float(s), ''
    for n, u in enumerate(s):
        if not (u.isdigit() or u == '.' or u == '-'):
            break
    try:
        num = float(s[:n])
    except ValueError as e:
        num = 0
    unit = s[n:]
    return num, unit

def check_bundle_componnet_ram(s:str):
    if not s:
        return ERROR_EMPTY
    if s[0] == '-':
        return "size must greater than 0."
    num, unit = str2value(s)
    error = ""
    if num <= 0:
        error += 'must be grater than 0. '
    unit_list = ['', "KB", "KByte", "MByte", "MB"]
    if not unit in unit_list:
        error += "unit error."
    return error

def check_bundle_componnet_rom(s:str):
    if not s:
        return ERROR_EMPTY
    if s[0] == '-':
        return "'rom' size must greater than 0."
    num, unit = str2value(s)
    error = ""
    if num <= 0:
        error += 'rom must be grater than 0. '
    unit_list = ['', "KB", "KByte", "MByte", "MB"]
    if not unit in unit_list:
        error += "unit error."
    return error

def check_bundle_componnet_ast(ast:list):
    '''
    @note: ast 为 "adapted_system_type" 的缩写。
    '''
    if len(ast) == 0:
        return ERROR_EMPTY
    type = tuple(set(ast))
    if len(type) != len(ast):
        return 'adapted_system_type can not have duplicate type.'
    if len(ast) > 3:
        return 'adapted_system_type only 3 at most(mini,small,standard)'
    type_list = ["mini", "small", "standard"]
    error_type = []
    for i in ast:
        if i not in type_list:
            error_type.append(i)
    if error_type:
        return str(error_type) + ' not the correct type.'
    return ""

def check_bundle_componnet_feature(features:list, component:str):
    if len(features) == 0:
        return []
    error_list = []
    for ft in features:
        err = {}
        err[ft] = []
        if not '_' in ft:
            err[ft].append(FEATURE_SEPARATOR_ERROR)
            error_list.append(err)
            continue
        if not ft.startswith(component):
            err[ft].append(FEATURE_PREFIX_ERROR)
        if err[ft]:
            error_list.append(err)
    return error_list

def check_bundle_componnet_syscap(syscap:list):
    if len(syscap) == 0:
        return []
    error_list = []
    for i in syscap:
        err = {}
        if not i: # syscap rmpty
            err[i] = 'syscap string ' + ERROR_EMPTY
            error_list.append(err)
            continue
        # check every syscap
        words = i.split('.')
        err[i] = []
        if words[0] != 'SystemCapability':
            err[i].append(SYSCAP_HEADER_ERROR)
        for j in words:
            if not j[0].isupper():
                err[i].append(PASCAL_CASING_STYLE)
        if len(words) < 3: # SystemCapability.Subsystem.Component[.Subcomponent]
            err[i].append(SYSCAP_FORMAT_ERROR)
        if err[i]:
            error_list.append(err)
    return error_list

def check_bundle_componnet_subsystem(subsystem:str) -> str:
    if not subsystem:
        return ERROR_EMPTY
    if not islower(subsystem):
        return SUBSYSTEM_MUST_LOWER
    # if subsystem.count('_'):
    #     return "SUBSYSTEM_NOT_USE_UNDERLINE"
    return ""

def check_bundle_component_name(componnet_name:str, name:str) -> str:
    if not componnet_name:
        return ERROR_EMPTY
    if '/' in name: # 'name' error, skip compare
        if componnet_name != name.split('/')[1]:
            return COMPONENT_NAME_ERROR
    return ""

def check_bundle_destPath(path) -> str:
    if not path:
        return ERROR_EMPTY
    if type(path) != str:
        return ONLY_ONE_DESTPATH
    return ""

def check_bundle_version(version:str) -> str:
    if len(version) < 3: # 3.1
        return VERSION_ERROR
    return ""

def check_bundle_name(name:str) -> str:
    if not name: # 为空
        return ERROR_EMPTY
    if '@ohos/' != name[:6]:
        return NAME_FORMAT_ERROR
    if len(name.split('/')[1]) < 1:
        return NAME_FORMAT_ERROR
    if len(name.split('/')[1]) > 63:
        return COMPONENT_NAME_TOO_LONG
    if not name.islower():
        return NAME_NOT_LOWER
    return ""

def check_bundle_json(path):
    bundle_file = open(path, 'r')
    try:
        bundle_json = json.load(bundle_file)
    except json.decoder.JSONDecodeError as e:
        print("'" + path + "'" + " is not a bundle.json file.")
        exit(1)
    bundle_file.close()
    error_field = {}
    # name
    if 'name' in bundle_json:
        re = check_bundle_name(bundle_json['name'])
        name_error = re
    else:
        name_error = NO_FIELD
    if name_error:
        error_field['name'] = name_error
    # version
    if 'version' in bundle_json:
        re = check_bundle_version(bundle_json['version'])
        version_error = re
    else:
        version_error = NO_FIELD
    if version_error:
        error_field['version'] = version_error
    # segment
    segmnet_error = {}
    if 'segment' in bundle_json:
        if 'destPath' in bundle_json['segment']:
            re = check_bundle_destPath(bundle_json['segment']['destPath'])
            if re:
                segmnet_error['destPath'] = re
        else:
            segmnet_error['destPath'] = NO_FIELD
    else:
        error_field['segment'] = NO_FIELD
    if segmnet_error:
        error_field['segment'] = segmnet_error
    # component
    component_error = {}
    if 'component' in bundle_json:
        component = bundle_json['component']
        # name
        if 'name' in component and 'name' in bundle_json:
            re = check_bundle_component_name(component['name'], bundle_json['name'])
            if re:
                component_error['name'] = re
        else:
            component_error['name'] = NO_FIELD
        # subsystem
        if 'subsystem' in component:
            re = check_bundle_componnet_subsystem(component['subsystem'])
            if re:
                component_error['subsystem'] = re
        else:
            component_error['subsystem'] = NO_FIELD
        # syscap
        if 'syscap' in component:
            re = check_bundle_componnet_syscap(component['syscap'])
            if re:
                component_error['syscap'] = re
        else:
            pass # syscap is optional
        # feature
        if 'feature' in component:
            re = check_bundle_componnet_feature(component['feature'], component['name'])
            if re:
                component_error['feature'] = re
        else:
            pass # feature is optional
        # adapted_system_type
        if 'adapted_system_type' in component:
            re = check_bundle_componnet_ast(component['adapted_system_type'])
            if re:
                component_error['adapted_system_type'] = re
        else:
            component_error['adapted_system_type'] = NO_FIELD
        # rom
        if 'rom' in component:
            re = check_bundle_componnet_rom(component['rom'])
            if re:
                component_error['rom'] = re
        else:
            component_error['rom'] = "no 'component:rom'"
        # ram
        if 'ram' in component:
            re = check_bundle_componnet_ram(component['ram'])
            if re:
                component_error['ram'] = re
        else:
            component_error['ram'] = "no 'component:ram' field"
        # deps
        if 'deps' in component:
            pass
        else:
            pass
    else:
        component_error = NO_FIELD
    if component_error:
        error_field['component'] = component_error
    # test
    # test_json = json.dumps(error_field, indent=4, separators=(', ', ': '))
    # print(test_json)
    return error_field

def get_all_bundle_json() -> list:
    cmd = 'find -name bundle.json -not -path "./out/*"'
    bundle_josn_list = os.popen(cmd).readlines()
    return bundle_josn_list

def export_to_xls(bundle_json:dict, output_path:str):
    raw = 1
    workbook = xlwt.Workbook(encoding='utf-8', style_compression=0)
    worksheet = workbook.add_sheet('bundle.json warnning', cell_overwrite_ok=True)
    # 标题栏
    worksheet.write(0, 0, '路径')
    worksheet.write(0, 1, '位置')
    worksheet.write(0, 2, '错误')
    # 设置列宽
    worksheet.col(0).width=256*48
    worksheet.col(1).width=256*35
    worksheet.col(2).width=256*60
    for bundle_path in bundle_json:
        # print(bundle_json[bundle_path]) # test
        error_dict = bundle_json[bundle_path]
        raw_begin = raw # 同一个bundle 的起始行
        for key in error_dict:
            if type(error_dict[key]) == dict:
                for filed_name in error_dict[key]:
                    worksheet.write(raw, 0, bundle_path)
                    worksheet.write(raw, 1, key + ': ' + filed_name)
                    try:
                        worksheet.write(raw, 2, str(error_dict[key][filed_name]))
                    except:
                        print(bundle_path)
                    raw += 1
            else:
                worksheet.write(raw, 0, bundle_path)
                worksheet.write(raw, 1, key)
                worksheet.write(raw, 2, error_dict[key])
                raw += 1
        raw_end = raw - 1 # 同一个bundle 的末尾行
        # 合并同样的 bundle 路径列
        # 设置单元格样式为居中
        style = xlwt.XFStyle()
        # 对齐设置
        ali = xlwt.Alignment()
        ali.vert = 0x01 # 垂直对齐方式为居中
        style.alignment = ali
        worksheet.write_merge(raw_begin, raw_end, 0, 0, bundle_path, style)
        # workbook.save('./test/测试.xls') # test
        # exit(0) # test
    out_path = os.path.normpath(output_path) + '/' + 'all_bundle_error.xls'
    workbook.save(out_path)
    print("Please check " + out_path)

def export_to_json(all_errors:dict, output_path:str):
    all_error_json = json.dumps(all_errors, indent=4, separators=(', ', ': '))
    out_path = os.path.normpath(output_path) + '/' + 'all_bundle_error.json'
    with open(out_path, 'w') as f:
        f.write(all_error_json)
    print("Please check " + out_path)

def check_all_bundle_json(project_path) -> dict:
    script_path = os.getcwd()
    os.chdir(project_path)
    all_bundle = get_all_bundle_json()
    all_error = {}
    for bundle in all_bundle:
        bundle_path = bundle.strip('\n')
        bundle_error = check_bundle_json(bundle_path)
        if bundle_error:
            all_error[bundle_path] = bundle_error
    os.chdir(script_path)
    return all_error
    '''
    # export xml
    # all_error_xml = dicttoxml.dicttoxml(all_error)
    # dom = parseString(all_error_xml)
    # with open('all_bundle_error.xml', 'w') as f:
    #     f.write(dom.toprettyxml())
    # print("Please check ./all_bundle_error.xml")
    '''

def is_project(path) -> bool:
    p = os.path.normpath(path)
    return os.path.exists(p + '/' + '.repo/manifests')

def parse_args():
    parser = argparse.ArgumentParser()
    # exclusive output format
    ex_format = parser.add_mutually_exclusive_group()
    ex_format.add_argument("--xls", help="output format: xls(default).",
                           action="store_true")
    ex_format.add_argument("--json", help="output format: json.",
                           action="store_true")
    # exclusive input
    ex_input = parser.add_mutually_exclusive_group()
    ex_input.add_argument("-P", "--project", help="project root path.", type=str)
    ex_input.add_argument("-p", "--path", help="bundle.json path list.", nargs='+')
    args = parser.parse_args()
    if args.project:
        ohos_path = args.project
        if is_project(ohos_path):
            all_errors = check_all_bundle_json(ohos_path)
            if args.json:
                export_to_json(all_errors, '.')
            else:
                export_to_xls(all_errors, '.')
        else:
            print("'" + ohos_path + "' is not a oopeharmony project.")
            exit(1)
    elif args.path:
        bundle_list_error = {}
        for bundle_json_path in args.path:
            error_field = check_bundle_json(bundle_json_path)
            if error_field:
                bundle_list_error[bundle_json_path] = error_field
        test_json = json.dumps(bundle_list_error, indent=4, separators=(', ', ': '))
        print(test_json)
    else:
        print("use '-h' get help.")

if __name__ == '__main__':
    parse_args()
